﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using EnvDTE;
using Microsoft.VisualStudio.Shell;
using Muel.TypeScriptFacade.Core;
using Muel.TypeScriptFacade.Core.Configuration;
using VSLangProj;
using VSLangProj80;

namespace Muel.TypeScriptFacade.VisualStudio.Generator
{
    [ComVisible(true)]
    [Guid("B702F79A-731D-4E7E-875B-021405D73415")]
    [CodeGeneratorRegistration(typeof(TypeScriptFacadeGenerator), "TypeScript Facade Generator", vsContextGuids.vsContextGuidVCSProject, GeneratesDesignTimeSource = true)]
    [ProvideObject(typeof(TypeScriptFacadeGenerator))]
    public class TypeScriptFacadeGenerator : BaseCodeGeneratorWithSite
    {
#pragma warning disable 0414

        // The name of this generator, which must match the class name and is used for the 'Custom Tool' property of project item
        internal static string name = "TypeScriptFacadeGenerator";

#pragma warning restore 0414

        protected override string GetDefaultExtension()
        {
            return ".ts";
        }

        protected override byte[] GenerateCode(string inputFileContent)
        {
            try
            {
                // load the settings
                TypeScriptFacadeSettings settings;
                try
                {
                    settings = TypeScriptFacadeSettings.Deserialize(inputFileContent);
                }
                catch (Exception ex)
                {
                    // if the XML is badly formatted then generate an error for VS and step out
                    this.GeneratorError(4, "InvalidSchemaError " + ex.ToString(), 0, 0);
                    return null;
                }
                this.CodeGeneratorProgress.Progress(50, 100);

                // validate the settings
                var validationErrors = settings.Validate();
                if (validationErrors.Any())
                {
                    foreach (ValidationError error in validationErrors)
                    {
                        this.GeneratorError(4, error.ErrorMessage, 0, 0);
                    }
                    return null;
                }

                // instantiate the manager
                TypeScriptServiceManager manager = new TypeScriptServiceManager(settings);

                EnvDTE.Project project = GetProject();
                string assemblyPath = GetOutputAssemblyPath(project);

                if (!File.Exists(assemblyPath))
                {
                    this.GeneratorWarning(4, "Unable to find compiled assembly", 0, 0);
                    return null;
                }

                // to avoid locking the assembly for the build we need to read it to a byte array then load that
                Assembly assembly = LoadAssemblyAsByteArray(assemblyPath);

                // we also need to search the project for .asmx files to generate URL hints for the service facade generator
                IEnumerable<TypeScriptServiceUrlHint> hints = FindServiceUrlsInItems(project.ProjectItems);

                // load the assembly into the manager with the URL hints
                manager.Load(assembly, hints);

                // get any initialisation messages
                var messages = manager.GetMessages();
                if (messages.Any())
                {
                    foreach (var message in messages)
                    {
                        if (message.MessageLevel == TypeScriptFacadeMessageLevel.Warning)
                        {
                            this.GeneratorWarning(4, message.ToString(), 0, 0);
                        }
                        else if (message.MessageLevel == TypeScriptFacadeMessageLevel.Error)
                        {
                            this.GeneratorError(4, message.ToString(), 0, 0);
                        }
                    }
                }
                if (messages.Any(msg => msg.MessageLevel == TypeScriptFacadeMessageLevel.Error))
                {
                    return null;
                }

                string code = manager.ToTypeScript();

                Encoding enc = Encoding.UTF8;
                byte[] bytes = enc.GetBytes(code);

                this.CodeGeneratorProgress.Progress(100, 100);

                return bytes;
            }
            catch (Exception ex)
            {
                this.GeneratorError(4, ex.ToString(), 0, 0);
                return null;
            }
        }


        private IEnumerable<TypeScriptServiceUrlHint> FindServiceUrlsInItems(EnvDTE.ProjectItems items)
        {
            return FindServiceUrlsInItems(items, string.Empty);
        }
        private IEnumerable<TypeScriptServiceUrlHint> FindServiceUrlsInItems(EnvDTE.ProjectItems items, string currentFolder)
        {
            // loop through each project item
            foreach (ProjectItem item in items)
            {
                // recurse through any physical folders in the project
                if (item.Kind == Constants.vsProjectItemKindPhysicalFolder)
                {
                    string subFolder = string.Format("{0}/{1}", currentFolder, item.Name.Trim('/', '\\'));
                    var hints = FindServiceUrlsInItems(item.ProjectItems, subFolder);
                    foreach (var hint in hints)
                    {
                        yield return hint;
                    }
                }
                else if (item.Kind == Constants.vsProjectItemKindPhysicalFile)
                {
                    string fileExtension = Path.GetExtension(item.Name).ToLower();
                    string serviceUrl = string.Format("{0}/{1}", currentFolder, item.Name);
                    string serviceClass = null;

                    if (fileExtension == ".asmx")
                    {
                        // if we've found an .asmx file then locate the class from the .asmx
                        serviceClass = GetClassNameFromAsmx(item.Properties.Item("FullPath").Value.ToString());
                    }
                    else if (fileExtension == ".svc")
                    {
                        // if we've found an .asmx file then locate the class from the .svc
                        serviceClass = GetClassNameFromSvc(item.Properties.Item("FullPath").Value.ToString());
                    }

                    if (!string.IsNullOrEmpty(serviceClass) &&
                        !string.IsNullOrEmpty(serviceUrl))
                    {
                        yield return new TypeScriptServiceUrlHint(serviceUrl, serviceClass);
                    }
                }
            }
        }

        private string GetClassNameFromAsmx(string filePath)
        {
            return GetClassNameFromFile(filePath, "Class");
        }

        private string GetClassNameFromSvc(string filePath)
        {
            return GetClassNameFromFile(filePath, "Service");
        }

        private string GetClassNameFromFile(string filePath, string classXmlAttributeName)
        {
            string fileContents;
            using (FileStream stream = File.OpenRead(filePath))
            {
                using (StreamReader reader = new StreamReader(stream))
                {
                    fileContents = reader.ReadToEnd();
                    reader.Close();
                }
            }

            // use a regular expression to extract the class name from the file contents
            string pattern = string.Format(@"{0}[\s]*=[\s]*""([^""]*)""", classXmlAttributeName);
            Regex regex = new Regex(pattern);
            MatchCollection matches = regex.Matches(fileContents);
            if (matches.Count > 1)
            {
                GeneratorWarning(4, string.Format("Found multiple matches for service class in {0}", filePath), 0, 0);
                return string.Empty;
            }
            else if (matches.Count == 0)
            {
                GeneratorWarning(4, string.Format("Found 0 matches for service class in {0}", filePath), 0, 0);
                return string.Empty;
            }
            else
            {
                string className = matches[0].Groups[1].Value;
                return className;
            }
        }
        
        private Assembly LoadAssemblyAsByteArray(string assemblyPath)
        {
            byte[] assemblyBytes = null;
            using (Stream fileStream = File.OpenRead(assemblyPath))
            {
                byte[] buffer = new byte[16 * 1024];
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    int read;
                    while ((read = fileStream.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        memoryStream.Write(buffer, 0, read);
                    }
                    assemblyBytes = memoryStream.ToArray();
                }
            }
            try
            {
                Assembly assembly = Assembly.Load(assemblyBytes);
                return assembly;
            }
            catch
            {
                return null;
            }
        }

        private string GetOutputAssemblyPath(EnvDTE.Project project)
        {
            string fullPath = project.Properties.Item("FullPath").Value.ToString();
            string outputPath = project.ConfigurationManager.ActiveConfiguration.Properties.Item("OutputPath").Value.ToString();
            string outputDir = Path.Combine(fullPath, outputPath);
            string outputFileName = project.Properties.Item("OutputFileName").Value.ToString();
            string assemblyPath = Path.Combine(outputDir, outputFileName);
            return assemblyPath;
        }
    }
}
